/*********************************************************************\
*                                                                     *
*         Jamper - Java XML mapper (visual XSLT generator)            *
*                Copyright (C) 2005 Filip Pavlovic                    *
*                    sqba@users.sourceforge.net                       *
*                                                                     *
*  This program is free software; you can redistribute it and/or      *
*  modify it under the terms of the GNU General Public License        *
*  as published by the Free Software Foundation; either version 2     *
*  of the License, or (at your option) any later version.             *
*                                                                     *
*  This program is distributed in the hope that it will be useful,    *
*  but WITHOUT ANY WARRANTY; without even the implied warranty of     *
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the      *
*  GNU General Public License for more details.                       *
*                                                                     *
*  You should have received a copy of the GNU General Public License  *
*  along with this program; if not, write to the Free Software        *
*  Foundation, Inc., 59 Temple Place - Suite 330, Boston,             *
*  MA 02111-1307, USA.                                                *
*                                                                     *
\*********************************************************************/


package sqba.jamper.map.tree;


import sqba.jamper.JamperFrame;
import sqba.jamper.ObjectPanel;
import sqba.jamper.map.ILinkable;
import sqba.jamper.map.object.DefaultLinkable;
import sqba.jamper.map.object.LinkLine;
import sqba.jamper.map.tree.model.HTMLTreeModel;
import sqba.jamper.map.tree.model.MapperTreeModel;
import sqba.jamper.map.tree.model.SAXTreeModel;
import sqba.jamper.map.tree.model.XSOMTreeModel;
import sqba.jamper.util.PathList;
import sqba.jamper.util.XMLMapperInit;
//import sqba.jamper.tree.model.XSDModel;

import java.io.File;
import java.io.IOException;

import java.util.List;
import java.util.Iterator;
import java.util.Enumeration;

import javax.swing.JOptionPane;
import javax.swing.JTree;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
//import javax.swing.JOptionPane;
//import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeNode;
//import javax.swing.tree.TreeModel;
//import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.DefaultTreeCellRenderer;
import java.awt.Point;
import java.awt.Graphics;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragSourceContext;
import java.awt.dnd.DnDConstants;
import java.awt.event.AdjustmentListener;
//import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;

import javax.swing.JMenu;
import javax.swing.AbstractAction;
//import java.lang.Thread;
//import javax.swing.tree.DefaultTreeModel;


/**
 * Main tree class
 *
 * @author	Filip Pavlovic
 * @version	1.0
 */
public class MapperTree extends JTree
{
	private MapperTreeNode	_treeRoot	= null;
	private DropTarget		_dropTarget = null;
	private DragSource		_dragSource = null;
	private TreePath		_dragPath	= null;
	private TreePath		_dropPath	= null;
	private int 			_scrollPos	= 0;
	private TreeScrollPane	_scrollPane	= null;
	private ObjectPanel		_mapObjView	= null;
	private boolean			_showValues	= false;
	private String			_uri		= "";

	/**
	 * JScrollPane derived class that enables listening for
	 * verticalScrollBar changes. It had to be overridden
	 * because verticalScrollBar property is private.
	 */
	class TreeScrollPane extends JScrollPane
	{
		/**
		 * Constructor
		 */
		public TreeScrollPane(Component cmp)
		{ super(cmp); }

		/**
		 * Sets vertical scroll bar adjustment listener.
		 * Necessary because verticalScrollBar property
		 * is private.
		 *
		 * @param al verticalScrollBar AdjustmentListener
		 */
		public void setListener(AdjustmentListener al)
		{ this.verticalScrollBar.addAdjustmentListener(al); }
	}

	class TreeDragSourceListener implements DragSourceListener
	{
		public void dragDropEnd (DragSourceDropEvent event)
		{
		    if ( event.getDropSuccess()){
		        repaint(); // Draw the new line
		    }
		    _dragPath = null;
		}

		public void dragEnter(DragSourceDragEvent event)
		{
			DragSourceContext context = event.getDragSourceContext();
			context.setCursor(DragSource.DefaultLinkDrop);
		}

		public void dragExit(DragSourceEvent event) {}
		public void dragOver(DragSourceDragEvent event) {}
		public void dropActionChanged(DragSourceDragEvent event) {}
	}

    class TreeDropTargetListener implements DropTargetListener
    {
		public void dragEnter (DropTargetDragEvent event)
		{
			//if(!event.isLocalTransfer())
			//if(!event.isDataFlavorSupported(MapperTreeNode.mapperTreeNodeFlavor))
			//	event.rejectDrag();
			/*if(isSameTree(event.getTransferable())) {
				//event.rejectDrag();
				return;
			}*/

			//event.acceptDrag (DnDConstants.ACTION_MOVE);
		}

		public void dragExit (DropTargetEvent event)
		{
			deselectAll( null );
			_dropPath = null;
		}

		public void dragOver (DropTargetDragEvent event)
		{
			Transferable transferable = event.getTransferable(); // JDK 1.5

			if(isSameTree(transferable)) {
				//event.rejectDrag();
				return;
			}

			if(!event.isDataFlavorSupported(MapperTreeNode.mapperTreeNodeFlavor) &&
				!event.isDataFlavorSupported(DefaultLinkable.linkableFlavor) &&
				!event.isDataFlavorSupported(DefaultLinkable.localLinkableFlavor))
				return;

		    Point loc = event.getLocation();
			//TreePath path = getPathForLocation(loc.x, loc.y);
		    TreePath path = getClosestPathForLocation(loc.x, loc.y);
			if (path != null) {
				_dropPath = path;
				setSelectionPath(path);

				ILinkable src = DefaultLinkable.getLinkable(transferable);

				MapperTreeNode dstNode = (MapperTreeNode)getLastSelectedPathComponent();

				if(dstNode.willAccept(src)) {
					dstNode.setTreePath(path);
					_mapObjView.setDragLine(src, dstNode);

					repaint();
					_mapObjView.repaint();
				}
			}
		}

		public synchronized void drop(DropTargetDropEvent event)
		{
			_dropPath = null;

			if(!event.isLocalTransfer()) {
				Transferable t = event.getTransferable();
				if(t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
					try {
						event.acceptDrop(event.getDropAction());
						Object txData = t.getTransferData(DataFlavor.javaFileListFlavor);

						if (!(txData instanceof List)) {
							event.rejectDrop();
							return;
						}

		                List lst = (List) txData;
		                if(!lst.isEmpty()) {
		                	File f = (File)lst.get(0);
		                	_mapObjView.getMap().clearLinks();
		                	load(f.getAbsolutePath());
		                }
		                event.dropComplete(true);
					} catch (UnsupportedFlavorException ufe) {
						System.out.println("UnsupportedFlavorException: " + ufe);
					} catch (IOException ioe) {
						System.out.println("IOException: " + ioe);
		            } catch (Throwable tr) {
		                event.rejectDrop();
		            }
				}
				return;
			}

			Transferable transferable = event.getTransferable();

			if(isSameTree(transferable)) {
				//event.rejectDrop();
				repaint();
				return;
			}

			ILinkable src = DefaultLinkable.getLinkable(transferable);

			//Point pt = event.getLocation();
			//TreePath pathTarget = getClosestPathForLocation(pt.x, pt.y);

			// Destination node
			TreePath path = getSelectionPath();
			MapperTreeNode dst = (MapperTreeNode)getLastSelectedPathComponent();
			dst.setTreePath(path);

			if(dst.willAccept(src)) {
				_mapObjView.addLink(src, dst);
				event.acceptDrop(event.getDropAction());
				event.dropComplete(true);
				if(dst.isOutput())
					dst.update();
				else if(src instanceof MapperTreeNode)
					((MapperTreeNode)src).update();
			}
		}

		public void dropActionChanged (DropTargetDragEvent event ) {}
	}

    class TreeDragGestureListener implements DragGestureListener
    {
		public void dragGestureRecognized(DragGestureEvent event)
		{
			TreePath path = getSelectionPath();
			if (path != null) {
				_dragPath = path;
				MapperTreeNode node = (MapperTreeNode)getLastSelectedPathComponent();
				if(node.willAccept(null)) {
					node.setTreePath(path);
					_dragSource.startDrag(event,
										DragSource.DefaultLinkDrop,
										node,
										new TreeDragSourceListener());
					repaint();
				}
			}
		}
	}

	class MyMouseListener implements MouseListener
	{
		public void mouseClicked(MouseEvent e)
		{
			Point pt = e.getPoint();
		    TreePath path = getClosestPathForLocation(pt.x, pt.y);
			if (path != null) {
				MapperTreeNode node = (MapperTreeNode)path.getLastPathComponent();
				JamperFrame._instance.objectSelected(node);
				if(node.isLinked() && !node.hitTest(pt)) {
					_mapObjView.getMap().deselectAll();
					node.select(true);
					Iterator iter = node.getLinks().iterator();
					while ( iter.hasNext() ) {
			    		LinkLine line = (LinkLine)iter.next();
			    		line.select(true);
			    	}
			    	repaint();
			    	_mapObjView.repaint();
				}
			}
		}
		public void mouseEntered(MouseEvent e) {}
		public void mouseExited(MouseEvent e) {}
		public void mousePressed(MouseEvent e) {}
		public void mouseReleased(MouseEvent e)
		{
			if (e.isPopupTrigger()) {
				Point pt = e.getPoint();
				TreePath path = getPathForLocation(pt.x, pt.y);
				if(path != null) {
					setSelectionPath(path);

					MapperTreeNode node = (MapperTreeNode)path.getLastPathComponent();
					JamperFrame._instance.objectSelected(node);

					// Create pop-up menu
					JPopupMenu popup = new JPopupMenu();
					JMenuItem menuItem = node.getMenu();
					if(null != menuItem) {
						popup.add(menuItem);
						MapperTree.this.repaint();
						popup.show(e.getComponent(), e.getX(), e.getY());
					}
				}
			}
		}
	}

	/**
	 * Constructor
	 * initializes the tree.
	 *
	 * @param sink determines whether the tree represents the output structure
	 */
	public MapperTree(boolean sink)
	{
		super(new MapperTreeNode("Root", null));

		this.putClientProperty("JTree.lineStyle", "Angled");
		DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();

        renderer.setLeafIcon(XMLMapperInit.loadIcon(XMLMapperInit.ICON_LEAF));
        renderer.setOpenIcon(XMLMapperInit.loadIcon(XMLMapperInit.ICON_OPEN));
        renderer.setClosedIcon(XMLMapperInit.loadIcon(XMLMapperInit.ICON_CLOSED));

 		this.setCellRenderer(renderer);

		TreeSelectionModel tsm = this.getSelectionModel();
		tsm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

		this._treeRoot = (MapperTreeNode)this.getModel().getRoot();
		this._scrollPane = new TreeScrollPane(this);

		_treeRoot.setTree(this);

		setBorder(null);
		this._scrollPane.setBorder(null);

		this._scrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT );

		// Initialize listeners
		this.addMouseListener(new MyMouseListener());

		_dropTarget = new DropTarget(this, new TreeDropTargetListener());
		_dragSource = new DragSource();
		_dragSource.createDefaultDragGestureRecognizer(this,
	    								DnDConstants.ACTION_MOVE,
	    								new TreeDragGestureListener());

		_scrollPane.setListener(new AdjustmentListener(){
			public void adjustmentValueChanged(AdjustmentEvent e){
				_scrollPos = e.getValue();
				repaint();
				if(_mapObjView != null)
					_mapObjView.repaint();
			}
		});

		if(sink) {
	        this.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
	        this._scrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
		}

		_scrollPane.setWheelScrollingEnabled(true);

		//ToolTipManager.sharedInstance().registerComponent(this);

		setEditable(sink); // Only output (sink) tree is editable
	}

    public void setMapObjectView(ObjectPanel mapObjView)	{ _mapObjView = mapObjView; }
    public JScrollPane getScrollPane()						{ return _scrollPane; }
    public String getURI()									{ return _uri; }

	public void paintComponent(Graphics g)
	{
		super.paintComponent(g);
		if(_dropPath != null)
			drawLink(g, _dropPath);
		if(_dragPath != null)
			drawLink(g, _dragPath);
		_treeRoot = (MapperTreeNode)this.getModel().getRoot();
		drawLinks(g, new TreePath(_treeRoot));
	}

	/**
	 * If expand is true, expands all nodes in the tree.
	 * Otherwise, collapses all nodes in the tree.
	 *
	 * @param parent tree node path, if null then root is assumed.
	 * @param expand If true, expands all nodes in the tree.
	 */
    public void expandAll(TreePath parent, boolean expand)
    {
    	if(null == parent)
    		parent = new TreePath(_treeRoot);
        // Traverse children
        TreeNode node = (TreeNode)parent.getLastPathComponent();
        if (node.getChildCount() >= 0) {
            for (Enumeration e=node.children(); e.hasMoreElements(); ) {
                TreeNode n = (TreeNode)e.nextElement();
                TreePath path = parent.pathByAddingChild(n);
		((MapperTreeNode)n).setTreePath(path);
                MapperTree.this.expandAll(path, expand);
            }
        }

        // Expansion or collapse must be done bottom-up
        if (expand) {
            MapperTree.this.expandPath(parent);
        } else {
            MapperTree.this.collapsePath(parent);
        }
    }

	/**
	 * If any of the ancestor nodes are collapsed,
	 * returns path to the first visible ancestor.
	 *
	 * @param path
	 *
	 * @return path to the first visible ancestor
	 */
    public TreePath getFirstVisibleAncestor(TreePath path)
    {
    	/*if(null == path) {
	    	System.out.println("null == path");
    		return null;
    	}*/
    	while(null!=path && !isVisible(path))
    		path = path.getParentPath();
    	return path;
    }

	/**
	 * Draws the line extension inside the tree view.
	 *
	 * @param g graphics object
	 * @param path tree node path
	 */
	public void drawLink(Graphics g, TreePath path)
	{
		MapperTreeNode node = (MapperTreeNode)path.getLastPathComponent();
		node.draw(g, this);
	}

	/**
	 * Recursively draws line extensions for all linked nodes.
	 *
	 * @param g graphics object
	 * @param parent tree node path
	 */
	private void drawLinks(Graphics g, TreePath parent)
    {
        if(parent == null)
        	return;

		MapperTreeNode node = (MapperTreeNode)parent.getLastPathComponent();

		if(node == null)
			return;

		if(node.isLinked())
			node.draw(g, this);

		// Traverse children
		Enumeration e = node.children();
		while ( e.hasMoreElements() ) {
			TreeNode n = (TreeNode)e.nextElement();
			TreePath path = parent.pathByAddingChild(n);
			drawLinks(g, path);
		}
    }

	/**
	 * Returns true if the transferable comes from the same tree.
	 *
	 * @param transferable Transferable object
	 *
	 * @return true if the transferable comes from the same tree
	 */
	public boolean isSameTree(Transferable transferable)
	{
		ILinkable src = DefaultLinkable.getLinkable(transferable);
		if(src instanceof MapperTreeNode) {
			MapperTreeNode node = (MapperTreeNode)src;
			if(node != null) {
				if(this.equals(node.getTree()))
					return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if the tree represents the output structure.
	 *
	 * @return true if the tree represents the output structure
	 */
	public boolean isSinkTree()
	{
		return (ComponentOrientation.RIGHT_TO_LEFT == getComponentOrientation());
	}

	/**
	 * Returns vertical scroll position.
	 *
	 * @return int, vertical scroll position.
	 */
	public int getScrollPos() { return _scrollPos; }

	/**
	 * Recursively de-selects all tree nodes.
	 */
	public void deselectAll(MapperTreeNode parent)
    {
    	if(parent == null)
    		parent = _treeRoot;

        parent.select(false);

        // Traverse children
		Enumeration e = parent.children();
		while ( e.hasMoreElements() ) {
			deselectAll((MapperTreeNode)e.nextElement());
		}

		repaint();
    }

	/**
	 * Returns root node.
	 *
	 * @return MapperTreeNode, root node.
	 */
	public MapperTreeNode getRootNode()	{ return _treeRoot; }

	public boolean getShowValues()	{ return _showValues; }

	public void setShowValues(boolean showValues)
	{
		_showValues = showValues;
		if(null != _treeRoot) {
			_treeRoot.update();
			expandAll(new TreePath(_treeRoot), true);
		}
	}
	
	public void setRoot(String path)
	{
		MapperTreeNode node = getNodeByXPath(path);
		if(null != node)
			setRoot(node);
	}
	
	public void setRoot(MapperTreeNode node)
	{
		MapperTreeModel tm = (MapperTreeModel)this.getModel();
		tm.setRoot(node);
     	node.setParent(null);
		this.expandAll(new TreePath(node), true);
	}
	
	public boolean isLoaded()
	{
		MapperTreeModel tm = (MapperTreeModel)this.getModel();
		return (null != tm ? tm.isLoaded() : false);
	}

	/**
	 * Returns the node corresponding to the given XPath.
	 *
	 * @param path XPath string
	 *
	 * @return MapperTreeNode
	 */
	public MapperTreeNode getNodeByXPath(String path)
	{
		//MapperTreeNode tmp = _treeRoot;
		MapperTreeNode tmp = (MapperTreeNode)this.getModel().getRoot();

		// Scroll through the XPath
		PathList pl = new PathList(path);
		Iterator iter = pl.iterator();
		while ( iter.hasNext() ) {
			String tmpName = (String)iter.next();
			//tmpName = tmpName.replaceAll("[1]", "");
			//tmpName = tmpName.replaceAll("\x5b1\x5d", "");
			//tmpName = tmpName.replaceAll("/[1/]", "");
			if(tmpName.length()>0) {
				//String s = tmp.getXPathName();
				if(!tmpName.equals(tmp.getXPathName())) {
					MapperTreeNode child = tmp.getChildByName(tmpName);
					if(null != child)
						tmp = child;
					else
					{
						tmp = null;
						break;
					}
				}
			}
		}

		return tmp;
	}

	public void load(String uri)
	{
		if(null == uri)
			return;

		MapperTreeModel newModel = null;

		_uri = uri;

		MapperTreeNode root = new MapperTreeNode(true, "Loading '" + uri + "'...", this);

		int len = uri.length();
		int x = uri.lastIndexOf('.')+1;
		// x < len?
		String ext = uri.substring(x, len);

		if(ext.equals("xml")) {
			newModel = new SAXTreeModel(root);
			//((SAXTreeModel)newModel).Load(uri);
			if(!isSinkTree())
				JamperFrame._instance.setTestFile(uri);
		} else if(ext.equals("xsd")) {
			newModel = new XSOMTreeModel(root);
			//((XSOMTreeModel)newModel).Load(uri);
		} else if(ext.equals("htm") || ext.equals("html")) {
			newModel = new HTMLTreeModel(root);
			//((HTMLTreeModel)newModel).Load(uri);
		}

		if(newModel != null)
		{
			newModel.Load(uri);
			this.setModel(newModel);
		}
		
		/*try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
		    e.printStackTrace();
		}*/
		expandAll(null, true);

		/*if(isSinkTree()) {
	        JScrollBar sb = _scrollPane.getHorizontalScrollBar();
	        //System.out.println(sb.getMaximum());
	        sb.setValue(sb.getMaximum());
	        //sb.setVisibleAmount(sb.getMaximum());
		}*/

		// Ovo ne radi zato shto se uchitava u posebnom thread-u...
		//tree.expandAll(new TreePath(newModel.getRoot()), true);
	}
	
	public TreeNode find(String str, TreePath parent)
	{
    	/*if(null == parent)
    		parent = new TreePath(_treeRoot);

    	// Traverse children
        TreeNode node = (TreeNode)parent.getLastPathComponent();
        if (node.getChildCount() >= 0)
        {
            for (Enumeration e=node.children(); e.hasMoreElements(); )
            {
                TreeNode n = (TreeNode)e.nextElement();
                TreePath path = parent.pathByAddingChild(n);
                ((MapperTreeNode)n).setTreePath(path);
                MapperTree.this.expandAll(path, expand);
            }
        }*/
		return null;
	}

    class MyFileMenu extends JMenu
    {
		public MyFileMenu(String name)
    	{
    		super(name);

			JMenuItem menuItem;

			menuItem = new JMenuItem("Load...");
	        menuItem.addActionListener(new AbstractAction() {
		        public void actionPerformed(ActionEvent e) {
		        	MapperTree.this.load(XMLMapperInit.browse(true));
		        }
		    });
			this.add(menuItem);

			menuItem = new JMenuItem("Find...");
	        menuItem.addActionListener(new AbstractAction() {
		        public void actionPerformed(ActionEvent e) {
		        	String str = JOptionPane.showInputDialog("Enter string to find", "");
		        	MapperTree.this.find(str, null);
		        }
		    });
			this.add(menuItem);
			
			/*menuItem = new JMenuItem("Save...");
	        menuItem.addActionListener(new AbstractAction() {
		        public void actionPerformed(ActionEvent e) {
		        	//MapperTree.this.save(XMLMapperInit.browseFile(true));
		        }
		    });
			this.add(menuItem);*/
		}
	}
/*
    class MyPopupMenu extends JPopupMenu
    {
		public MyPopupMenu(MapperTreeNode node)
    	{
    		if(null != node) {
				JMenuItem menuItem = node.getMenu();
				if(null != menuItem) {
					this.add(menuItem);
					MapperTree.this.repaint();
				}
			}
		}
	}
*/
	public JMenu getMenu()
		{ return new MyFileMenu((isSinkTree() ? "Destination" : "Source") + " Tree"); }
}
